/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.injection;

import java.io.*;
import org.objectweb.asm.*;

/**
 * Rudimentary class file parser which figures out the class name, super class name and if a specified marker interface or {@link Serializable} is implemented.
 * 
 * @author M. Hautle
 */
class ClassAnalyzer {
	/** The internal name of the {@link Serializable} interface */
	private static final String SERIALIZALBE_INTERFACE = Type.getInternalName(Serializable.class);

	/**
	 * The type of CONSTANT_Class constant pool items.
	 */
	private static final int CLASS = 7;

	/**
	 * The type of CONSTANT_Fieldref constant pool items.
	 */
	private static final int FIELD = 9;

	/**
	 * The type of CONSTANT_Methodref constant pool items.
	 */
	private static final int METH = 10;

	/**
	 * The type of CONSTANT_InterfaceMethodref constant pool items.
	 */
	private static final int IMETH = 11;

	/**
	 * The type of CONSTANT_String constant pool items.
	 */
	private static final int STR = 8;

	/**
	 * The type of CONSTANT_Integer constant pool items.
	 */
	private static final int INT = 3;

	/**
	 * The type of CONSTANT_Float constant pool items.
	 */
	private static final int FLOAT = 4;

	/**
	 * The type of CONSTANT_Long constant pool items.
	 */
	private static final int LONG = 5;

	/**
	 * The type of CONSTANT_Double constant pool items.
	 */
	private static final int DOUBLE = 6;

	/**
	 * The type of CONSTANT_NameAndType constant pool items.
	 */
	private static final int NAME_TYPE = 12;

	/**
	 * The type of CONSTANT_Utf8 constant pool items.
	 */
	private static final int UTF8 = 1;

	/** Access flag for interfaces. */
	private static final int INTERFACE = 0x0200;

	/** The internal name of the marker interface */
	private final String markerInterface;

	/**
	 * Default constructor.
	 * 
	 * @param markerInterface The marker interface
	 */
	public ClassAnalyzer(Class markerInterface) {
		this.markerInterface = Type.getInternalName(markerInterface);
	}

	/**
	 * Analyzes the given class
	 * 
	 * @param is Inputsream of the class file (this method closes it after it has finished)
	 * @return A {@link ClassInformation} Object containing the extracted informations
	 * @throws IOException If something goes wrong
	 */
	ClassInformation analyze(final InputStream is) throws IOException {
		final ClassInformation result = new ClassInformation();
		try {
			// jump to constant pool count
			is.skip(8);
			final int cpCnt = readUnsignedShort(is);
			final String[] cpStrings = new String[cpCnt];
			final int[] cpPointers = new int[cpCnt];
			// read the strings and class infos of the constant pool
			for (int i = 1; i < cpPointers.length; i++) {
				final int v = is.read();
				switch (v) {
					case UTF8:
						cpStrings[i] = readUTF(is);
						break;
					case CLASS:
						cpPointers[i] = readUnsignedShort(is);
						break;
					// skip the others
					// 2 byte entries
					case STR:
						is.skip(2);
						break;
					// 4 bytes entries
					case FIELD:
					case METH:
					case IMETH:
					case INT:
					case FLOAT:
					case NAME_TYPE:
						is.skip(4);
						break;
					// 8 byte entries
					// using two slots in the constant pool
					case LONG:
					case DOUBLE:
						is.skip(8);
						i++;
						break;
					default:
						throw new RuntimeException("Unknown Type " + v);
				}
			}
			// read access flags
			final int accessFlags = readUnsignedShort(is);
			// read class name
			result.internalName = cpStrings[cpPointers[readUnsignedShort(is)]];
			// read superclass name
			result.parentName = cpStrings[cpPointers[readUnsignedShort(is)]];
			// parse interface informations (if this is not an interface itself)
			final int intCnt = (accessFlags & INTERFACE) == 0 ? readUnsignedShort(is) : 0;
			for (int i = 0; i < intCnt; i++) {
				final String intName = cpStrings[cpPointers[readUnsignedShort(is)]];
				result.serializable |= SERIALIZALBE_INTERFACE.equals(intName);
				result.tracked |= markerInterface.equals(intName);
			}
		} finally {
			if (is != null)
				is.close();
		}
		return result;
	}

	/**
	 * Reads a unsigned short from the stream
	 * 
	 * @param in The inputstream to use
	 * @return A integer of the value
	 * @throws IOException If something goes wrong
	 */
	private static int readUnsignedShort(final InputStream in) throws IOException {
		return (in.read() << 8) + (in.read() << 0);
	}

	/**
	 * Reads a UTF8 String from the stream
	 * 
	 * @param in The inputstream to use
	 * @return A string
	 * @throws IOException If something goes wrong
	 */
	private static String readUTF(final InputStream in) throws IOException {
		final int utflen = readUnsignedShort(in);
		final byte[] bytearr = new byte[utflen];
		final char[] chararr = new char[utflen];

		int c, char2, char3;
		int count = 0;
		int chararr_count = 0;
		// read the string into the byte array
		while (count < utflen)
			count += in.read(bytearr, chararr_count + count, utflen - count);
		count = 0;

		while (count < utflen) {
			c = bytearr[count] & 0xff;
			if (c > 127)
				break;
			count++;
			chararr[chararr_count++] = (char) c;
		}

		while (count < utflen) {
			c = bytearr[count] & 0xff;
			switch (c >> 4) {
				case 0:
				case 1:
				case 2:
				case 3:
				case 4:
				case 5:
				case 6:
				case 7:
					/* 0xxxxxxx */
					count++;
					chararr[chararr_count++] = (char) c;
					break;
				case 12:
				case 13:
					/* 110x xxxx 10xx xxxx */
					count += 2;
					char2 = bytearr[count - 1];
					chararr[chararr_count++] = (char) (((c & 0x1F) << 6) | (char2 & 0x3F));
					break;
				case 14:
					/* 1110 xxxx 10xx xxxx 10xx xxxx */
					count += 3;
					char2 = bytearr[count - 2];
					char3 = bytearr[count - 1];
					chararr[chararr_count++] = (char) (((c & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0));
					break;
				default:
					/* 10xx xxxx, 1111 xxxx */
					throw new UTFDataFormatException("malformed input around byte " + count);
			}
		}
		// The number of chars produced may be less than utflen
		return new String(chararr, 0, chararr_count);
	}
}
